﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Hiro.Containers;
using Hiro.Interfaces;
using LinFu.Reflection.Emit;
using Mono.Cecil;
using Mono.Cecil.Cil;
using MethodAttributes = Mono.Cecil.MethodAttributes;

namespace Hiro
{
    /// <summary>
    /// Represents a type that can define an implementation for the <see cref="IMicroContainer"/> interface.
    /// </summary>
    public class ContainerBuilder : IContainerBuilder
    {
        /// <summary>
        /// The service emitter instance.
        /// </summary>
        private IServiceEmitter _serviceEmitter;

        /// <summary>
        /// Initializes a new instance of the <see cref="IServiceEmitter"/> class.
        /// </summary>
        public ContainerBuilder() : this(new ServiceEmitter())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="IServiceEmitter"/> class.
        /// </summary>
        /// <param name="serviceEmitter">The service emitter that will emit the necessary instructions to instantiate a service instance.</param>
        public ContainerBuilder(IServiceEmitter serviceEmitter)
        {
            _serviceEmitter = serviceEmitter;
        }
        /// <summary>
        /// Emits the <see cref="IMicroContainer.GetAllInstances"/> method
        /// </summary>
        /// <param name="containerType">The container type that will implement the <see cref="IMicroContainer"/> interface.</param>
        /// <param name="mainModule">The module that hosts the container.</param>
        /// <param name="dependencyMap">The <see cref="IDependencyMap"/> that describes the dependencies in the current application.</param>
        public void EmitGetAllInstances(TypeDefinition containerType, ModuleDefinition mainModule, IDependencyMap dependencyMap)
        {
            var methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot
                                   | MethodAttributes.Virtual;

            var newMethod = containerType.DefineMethod("GetAllInstances", methodAttributes, new Type[] { typeof(Type) });
            newMethod.SetReturnType(typeof(IEnumerable<object>));

            var IL = newMethod.GetILGenerator();
            var resultList = newMethod.AddLocal(typeof(List<object>));
            var newListCtor = mainModule.ImportConstructor<List<object>>(new Type[0]);
            var getInstanceMethod = (from MethodDefinition method in containerType.Methods
                                     where method.Name == "GetInstance"
                                     select method).First();

            var services = dependencyMap.AvailableServices;
            var serviceMap = services.GroupByType();

            var addMethod = mainModule.ImportMethod<List<object>>("Add", new Type[] { typeof(object) });
            var getTypeFromHandle = mainModule.ImportMethod<Type>("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static);

            IL.Emit(OpCodes.Newobj, newListCtor);
            IL.Emit(OpCodes.Stloc, resultList);

            foreach (var serviceType in serviceMap.Keys)
            {
                var currentType = mainModule.Import(serviceType);
                var currentServiceList = serviceMap[serviceType];

                var skipAdd = IL.Create(OpCodes.Nop);

                // Push the type onto the stack
                IL.Emit(OpCodes.Ldarg_1);
                IL.Emit(OpCodes.Ldtoken, currentType);

                // Match the service type
                IL.Emit(OpCodes.Call, getTypeFromHandle);
                IL.Emit(OpCodes.Ceq);
                IL.Emit(OpCodes.Brfalse, skipAdd);

                foreach (var currentService in currentServiceList)
                {
                    var activationPoint = currentService.ActivationPoint;

                    IL.Emit(OpCodes.Ldloc, resultList);

                    // Push the MicroContainer onto the stack
                    IL.Emit(OpCodes.Ldarg_0);
                    var dependency = currentService.TargetDependency;
                    var serviceTypeRef = mainModule.Import(dependency.ServiceType);

                    // Push the service type onto the stack            
                    IL.Emit(OpCodes.Ldtoken, serviceTypeRef);
                    IL.Emit(OpCodes.Call, getTypeFromHandle);

                    // Push the service name onto the stack
                    var pushServiceName = dependency.ServiceName == null ? IL.Create(OpCodes.Ldnull) : IL.Create(OpCodes.Ldstr, dependency.ServiceName);
                    IL.Append(pushServiceName);

                    // Instantiate the type
                    IL.Emit(OpCodes.Callvirt, getInstanceMethod);

                    // Box the type, if necessary
                    if (serviceType.IsValueType)
                        IL.Emit(OpCodes.Box, mainModule.Import(serviceType));

                    IL.Emit(OpCodes.Callvirt, addMethod);
                }

                IL.Append(skipAdd);
            }

            var enumerableType = mainModule.ImportType<IEnumerable<object>>();
            IL.Emit(OpCodes.Ldloc, resultList);
            IL.Emit(OpCodes.Isinst, enumerableType);
            IL.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Emits the <see cref="IMicroContainer.GetInstance"/> method.
        /// </summary>
        /// <param name="containerType">The container type that will implement the <see cref="IMicroContainer"/> interface.</param>
        /// <param name="mainModule">The module that hosts the container.</param>
        /// <param name="dependencyMap">The <see cref="IDependencyMap"/> that describes the dependencies in the current application.</param>
        public void EmitGetInstance(TypeDefinition containerType, ModuleDefinition mainModule, IDependencyMap dependencyMap)
        {
            var methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot
                                   | MethodAttributes.Virtual;

            var parameterTypes = new Type[] { typeof(Type), typeof(string) };
            var services = dependencyMap.AvailableServices;
            var methodName = "GetInstance";

            MethodDefinition newMethod = containerType.DefineMethod(methodName, methodAttributes, parameterTypes);

            var IL = newMethod.GetILGenerator();
            var endLabel = IL.Create(OpCodes.Nop);

            // Group the services by type
            var serviceMap = services.GroupByType();

            var getTypeFromHandle = mainModule.ImportMethod<Type>("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static);
            var stringEquals = mainModule.ImportMethod<string>("op_Equality", BindingFlags.Public | BindingFlags.Static);

            foreach (var serviceType in serviceMap.Keys)
            {
                var currentType = mainModule.Import(serviceType);
                var currentServiceList = serviceMap[serviceType];

                foreach (var currentService in currentServiceList)
                {
                    var dependency = currentService.TargetDependency;
                    var serviceName = dependency.ServiceName;
                    var activationPoint = currentService.ActivationPoint;

                    var skipCreate = IL.Create(OpCodes.Nop);

                    // Push the type onto the stack
                    IL.Emit(OpCodes.Ldarg_1);
                    IL.Emit(OpCodes.Ldtoken, currentType);

                    // if (serviceType == currentService) {
                    IL.Emit(OpCodes.Call, getTypeFromHandle);
                    IL.Emit(OpCodes.Ceq);
                    IL.Emit(OpCodes.Brfalse, skipCreate);

                    // Push the service name onto the stack
                    var pushName = serviceName == null
                                       ? IL.Create(OpCodes.Ldnull)
                                       : IL.Create(OpCodes.Ldstr, serviceName);

                    IL.Append(pushName);

                    // Push the service name argument onto the stack
                    IL.Emit(OpCodes.Ldarg_2);

                    // Compare the two strings
                    IL.Emit(OpCodes.Call, stringEquals);
                    IL.Emit(OpCodes.Brfalse, skipCreate);

                    _serviceEmitter.EmitService(newMethod, IL, mainModule, currentService, dependencyMap);

                    // }
                    IL.Emit(OpCodes.Br, endLabel);
                    IL.Append(skipCreate);
                }
            }

            // Emit the default case
            IL.Emit(OpCodes.Ldnull);

            IL.Append(endLabel);
            IL.Emit(OpCodes.Ret);
        }        
    }
}
